#include "simple/io/udp_socket.h"

#include <iostream>
#include <chrono>
#include <thread>
#include <random>
#include <cassert>

using namespace std::literals;

int main(int argc, char const* argv[]) try
{
	auto address_result = argc == 3
		? simple::io::make_address(argv[1], argv[2])
		: simple::io::make_address("stun1.l.google.com", "19302")
	;

	if(std::holds_alternative<simple::io::name_unresolved>(address_result))
	{
		std::cout << "Name unresolved!" << '\n';
		return 0;
	}

	const auto& address = get<0>(address_result);
	std::cout << to_string(address) << '\n';

	auto s = get<0>(simple::io::open_udp_socket());
	std::vector<std::byte> request;

	// i hate htonl and the likes and the whole mental baggage that comes with
	// em.  the protocol is big endian, which means it's just the way you would
	// write it, nothing special, little endian is the special backwards one, so
	// if you just shift yer bytes into yer registers you shouldn't need to
	// give a single damn of what the endianess of your machine is or whatever
	// and if you want things to be fast, just define a protocol that matches the
	// endianess of your machine, like duh, why do you have to ruin everythin

	// binding request
	request.push_back(std::byte(0));
	request.push_back(std::byte(1));

	// message length 0?
	request.push_back(std::byte(0));
	request.push_back(std::byte(0));

	// magic cookie ?
	request.push_back(std::byte(0x21));
	request.push_back(std::byte(0x12));
	request.push_back(std::byte(0xa4));
	request.push_back(std::byte(0x42));

	// random transaction id
	auto rand = std::random_device{};
	std::uniform_int_distribution byte_dist(0,255);
	for(int i = 12; i --> 0;)
		request.push_back(std::byte(byte_dist(rand)));

	get<0>(simple::io::send(s, address, simple::io::as_byte_view(request)));

	std::vector<std::byte> response;
	response.resize(2048);
	auto frame_limit = 200;
	while(frame_limit --> 0)
	{
		auto br = simple::io::as_byte_range(response);
		auto received = get<0>(simple::io::receive(s, br));
		if(received != br.begin())
		{
			auto total_length = received - br.begin();
			std::cout << "Received " << total_length << " bytes" << '\n';
			auto reader = br.begin();
			// binding response type
			assert(*reader++ == std::byte(1));
			assert(*reader++ == std::byte(1));

			unsigned int message_length = 0;
			message_length |= (unsigned int)(*reader++);
			message_length <<= 8;
			message_length |= (unsigned int)(*reader++);
			std::cout << "Response message lenght: " << message_length << '\n';

			// magic cookie and transaction id repeat
			assert(std::equal(request.end() - 16, request.end(), reader));
			reader += 16;

			// TODO: handle plain address and jump over other attributes
			unsigned int attribute_type = 0;
			attribute_type |= (unsigned int)(*reader++);
			attribute_type <<= 8;
			attribute_type |= (unsigned int)(*reader++);

			std::cout << "Attribute type: "
				<< std::hex << std::showbase << attribute_type
			<< std::dec << '\n';

			unsigned int attribute_length = 0;
			attribute_length |= (unsigned int)(*reader++);
			attribute_length <<= 8;
			attribute_length |= (unsigned int)(*reader++);

			std::cout << "Attribute length: " << attribute_length << '\n';

			++reader; // skip unused byte
			assert(*reader++ == std::byte(1)); // address family is IPv4

			// port is xored with higher word of magic cookie
			unsigned int port = 0;
			port |= (unsigned int)(*reader++) ^ 0x21;
			port <<= 8;
			port |= (unsigned int)(*reader++) ^ 0x12;

			// ip is xored with the whole of magic cookie for v4
			std::array<unsigned int, 4> ip{};
			ip[0] |= (unsigned int)(*reader++) ^ 0x21;
			ip[1] |= (unsigned int)(*reader++) ^ 0x12;
			ip[2] |= (unsigned int)(*reader++) ^ 0xa4;
			ip[3] |= (unsigned int)(*reader++) ^ 0x42;

			std::cout << "Finally our public address is: " <<
				ip[0] << '.' << ip[1] << '.' << ip[2] << '.' << ip[3]
				<< ':' << port << '\n';

			break;
		}
		std::this_thread::sleep_for(16ms);
	}
	return 0;
}
catch(...)
{
	perror("omaaaagooot");
	throw;
}
